#include "Device.h"
#include "utils.h"

#include <cassert>
#include <iostream>
#include <cstdlib>
#include <algorithm>
#include <signal.h>


using namespace std;

void* daemon_exec_func(void *thread_args);



// ---------------------------------------------------------->
//
//				Public interface implementation
//
// ---------------------------------------------------------->

Device:: Device():
		_initialized(false),
		_on_closing(false),
		_daemon_thread_done_work(true),
		_cond_var_created(false),
		_next_id_generate(0)
{
	DBG_PRINT("Device constructor was called!\n");
}

Device:: ~Device()
{
	DBG_PRINT("device was deleted!\n");

	if ( pthread_mutex_destroy( & _context_lock ) )
	{
		ERR_PRINT("pthread_mutex_destroy() failed!\n");
		cerr<< "system error" << endl;
	}
}

result_t Device:: create_device()
{
	if (  pthread_mutex_init( & _context_lock, NULL ) )
	{
		return ERROR_SYSTEM_CALL;
	}

	_initialized = false;

	return OPERATION_SUCCESS;
}

result_t Device:: init_device( char *filename )
{
	result_t result = OPERATION_SUCCESS;

	assert( NULL != filename );

	CONTEXT_LOCK( _context_lock );

	if ( _initialized )
	{
		result =  ERROR_DEVICE_ALREADY_INITIALIZED;

		goto end;
	}

	// mark the device as initialized so there won't be two consecutive init calls
	_on_closing = false;
	_daemon_thread_done_work = true;

	if ( pthread_cond_init( & _queue_not_empty, NULL) )
	{
		ERR_PRINT("pthread_cond_init() failed!\n");
		result =  ERROR_SYSTEM_CALL;

		goto end;
	}
	else
	{
		_cond_var_created = true;
	}

	_outfile.open( filename, ios_base::out | ios_base::app);

	if ( _outfile.fail() )
	{
		ERR_PRINT("error while opening output file!\n");
		result = ERROR_IO;

		goto end;
	}

	if ( pthread_create( & _daemon_thread, NULL, daemon_exec_func, (void*) this ) )
	{
		ERR_PRINT("pthread_create() failed!\n");
		result =  ERROR_SYSTEM_CALL;

		goto end;
	}
	else
	{
		_daemon_thread_done_work = false;
	}

	// make sure all the resources of the thread will be freed on termination
	if ( pthread_detach( _daemon_thread ) )
	{
		ERR_PRINT("pthread_detach() failed!\n");
		result =  ERROR_SYSTEM_CALL;

		goto end;
	}

	_initialized = true;

end:
	CONTEXT_UNLOCK( _context_lock );

	return result;
}

result_t Device:: device_stop()
{
	result_t result = OPERATION_SUCCESS;

	CONTEXT_LOCK( _context_lock );

	DBG_PRINT("closing the device\n");

	_on_closing = true;

	// signal the daemon thread to woke up in case it's wait on request queue
	if ( _cond_var_created )
	{
		if ( pthread_cond_signal( & _queue_not_empty ) )
		{
			ERR_PRINT("pthread_cond_signal() failed!\n");
			result = ERROR_SYSTEM_CALL;
		}
	}

	CONTEXT_UNLOCK( _context_lock );

	return result;
}

result_t Device:: device_release_resources(bool force_release)
{
	Request *request = NULL;
	result_t result = OPERATION_SUCCESS;

	if ( force_release )
	{
		device_stop(); // make the daemon thread stop
	}

	CONTEXT_LOCK( _context_lock );

	_initialized = false;

	// make sure there are no requests that haven't been released
	while ( ! _queue.empty() )
	{
		request = _queue.front();
		_queue.pop();

		if ( OPERATION_SUCCESS != request->request_release_resources(force_release) )
		{
			result = ERROR_SYSTEM_CALL;
		}

		delete request;
	}


	if ( force_release )
	{
		// in case we are at forced release - we halt until the daemon thread terminate
		CONTEXT_UNLOCK_NO_RETURN( _context_lock, result );

		// busy wait until the daemon thread stop touch the database
		while ( ! _daemon_thread_done_work )
			;

		CONTEXT_LOCK_NO_RETURN( _context_lock, result );
	}

	_served_requests.clear();
	_requests.clear();

	if ( _outfile.is_open() )
	{
		_outfile.close();

		if ( _outfile.fail() )
		{
			ERR_PRINT("error while closing output file!\n");
			result = ERROR_SYSTEM_CALL;
		}
	}

	if ( _cond_var_created )
	{
		if ( pthread_cond_destroy ( & _queue_not_empty ) )
		{
			ERR_PRINT("pthread_cond_destroy() failed!\n");
			result = ERROR_SYSTEM_CALL;
		}
		else
		{
			_cond_var_created = false;
		}
	}

	CONTEXT_UNLOCK_NO_RETURN( _context_lock, result );

	return result;
}

result_t Device:: add_request( char* buffer, uint length, uint *id )
{
	Request *request = NULL;
	result_t result = OPERATION_SUCCESS;

	assert( NULL != buffer && NULL != id );

	CONTEXT_LOCK( _context_lock );

	if ( ! _initialized )
	{
		ERR_PRINT("write request to an uninitialized device!\n");
		result =  ERROR_DEVICE_HASNT_INITIALIZED;
		free(buffer);
		goto end;
	}

	if ( _on_closing )
	{
		result = ERROR_DEVICE_CLOSED;
		ERR_PRINT("write request to a closed device!\n");
		free(buffer);
		goto end;
	}

	*id = next_request_id();

	request = new Request(buffer, length, *id, result);
	if ( OPERATION_SUCCESS != result )
	{
		push_to_reserved_heap( *id );

		request->request_release_resources();
		delete request;

		goto end;
	}

	// add the request to the device database
	_requests.insert( pair<uint,Request *>(*id,request) );
	_queue.push(request);

	DBG_PRINT("Request with ID #%u and buffer #%p with length %u was added\n",
			*id,
			buffer,
			length);

	// wake up the daemon thread if it's wait for a request
	if ( pthread_cond_signal( & _queue_not_empty ) )
	{
		ERR_PRINT("pthread_cond_signal() failed!\n");
		result = ERROR_SYSTEM_CALL;
		goto end;
	}

end:
	CONTEXT_UNLOCK( _context_lock );

	return result;
}

result_t Device:: wait_request(uint id)
{
	result_t result = OPERATION_SUCCESS;
	map<unsigned int, Request*>:: iterator it;
	Request *request = NULL;

	CONTEXT_LOCK( _context_lock );

	if ( ! _initialized )
	{
		result =  ERROR_DEVICE_HASNT_INITIALIZED;
		goto unlock;
	}

	if ( id >= _next_id_generate )
	{
		ERR_PRINT("wait request for an unknown request id was received!\n");
		result = ERROR_UNKNOWN_REQUEST;
		goto unlock;
	}

	it = _requests.find( id );
	if ( _requests.end() != it )
	{
		// the request found in the requests map - it's hasn't been served
		assert( NULL != it->second );
		request = it->second;

		// the request's context will be unlocked after waiting to it.
		result = request->lock_request();
		if ( OPERATION_SUCCESS != result )
		{
			ERR_PRINT("lock_request() failed!\n");
			goto	unlock;
		}

	}

unlock:
	CONTEXT_UNLOCK( _context_lock );

	if ( NULL != request )
	{
		// the request hasn't been served - wait until it flushed
		return request->join_request();
	}

	return result;
}

result_t Device:: was_writen(uint id)
{
	result_t result = OPERATION_SUCCESS;
	map<unsigned int, Request*>:: iterator it;

	CONTEXT_LOCK( _context_lock );

	if ( ! _initialized )
	{
		result = ERROR_DEVICE_HASNT_INITIALIZED;
		goto end;
	}

	// heuristic - check if we already generated ID greater than the given one
	if ( id >= _next_id_generate )
	{
		result =  ERROR_UNKNOWN_REQUEST;
		goto end;
	}

	// check whether the request waits to be served
	it = _requests.find( id );

	if ( _requests.end() == it )
	{
		/*
		 *  the request isn't find in the waiting queue - it must be in the served
		 *  heap - we already checked that the given ID already been generated
		 */
		result = REQUEST_WAS_WRITEN;
	}
	else
	{
		// the request id is find in the waiting queue - so it wan't written
		result = REQUEST_WASNT_WRITEN;
	}

end:
	CONTEXT_UNLOCK( _context_lock );

	return result;
}


// ---------------------------------------------------------->
//
//				Private methods implementation
//
// ---------------------------------------------------------->

result_t Device:: is_on_closing(bool &on_closing)
{
	CONTEXT_LOCK( _context_lock );

	on_closing = _on_closing;

	CONTEXT_UNLOCK( _context_lock );

	return OPERATION_SUCCESS;
}

result_t Device:: remove_request( uint id )
{
	CONTEXT_LOCK( _context_lock );

	_requests.erase( id );

	CONTEXT_UNLOCK( _context_lock );

	return OPERATION_SUCCESS;
}

result_t Device:: push_to_reserved_heap(uint id)
{
	CONTEXT_LOCK( _context_lock );

	DBG_PRINT("ID #%u was enqueued to the heap\n", id);
	_served_requests.push_back(id);
	push_heap( _served_requests.begin(), _served_requests.end(), greater<uint>());

	CONTEXT_UNLOCK( _context_lock );

	return OPERATION_SUCCESS;
}

result_t Device:: wait_queue_not_empty()
{
	return wait_condition_var( & _context_lock, & _queue_not_empty, NULL, false );
}

// assume context is locked before calling this method
uint Device:: next_request_id()
{
	if ( _served_requests.empty() )
	{
		return _next_id_generate++;
	}

	uint id = _served_requests.front();

	DBG_PRINT("ID #%u was popped from the heap\n", id);

	pop_heap ( _served_requests.begin(), _served_requests.end(), greater<uint>() );
	_served_requests.pop_back();

	return id;
}

result_t Device:: pop_next_request( Request* &request)
{
	result_t result = OPERATION_SUCCESS;
	request = NULL;

	CONTEXT_LOCK( _context_lock );

	while( NULL == request )
	{
		if ( _queue.empty() )
		{
			if ( _on_closing )
			{
				break;
			}
			if ( OPERATION_SUCCESS != (result = wait_queue_not_empty()) )
			{
				ERR_PRINT("wait_queue_not_empty() failed!\n");
				break;
			}
		}
		else
		{
			request = _queue.front();
			_queue.pop();
		}

	}


	CONTEXT_UNLOCK( _context_lock );


	return OPERATION_SUCCESS;
}


// ---------------------------------------------------------->
//
//		Daemon thread execution function implementation
//
// ---------------------------------------------------------->

void* daemon_exec_func(void *thread_args)
{
	assert( NULL != thread_args );

	result_t result;
	Request *request = NULL;
	char *buf;
	uint length;
	uint id;

	Device *dev = (Device *) thread_args;

	DBG_PRINT("daemon thread woke up!\n");


	while( OPERATION_SUCCESS == (result = dev->pop_next_request(request))
			&& NULL != request )
	{

		id = request->get_request_id();
		request->get_buffer_info(buf, length);

		if ( length > 0 )
		{
			dev->_outfile.write ( buf, length );

			if ( dev->_outfile.bad() )
			{
				ERR_PRINT("writing to output file failed!\n");
				result = ERROR_IO;
			}
		}

		/*
		 * now we can remove the request from the map of requests so
		 * it will be marked as served
		 */
		if ( OPERATION_SUCCESS != dev->remove_request( id ) )
		{
			ERR_PRINT("remove_request() failed!\n");
			result = ERROR_SYSTEM_CALL;
		}

		if ( OPERATION_SUCCESS != request->request_release_resources( false ) )
		{
			ERR_PRINT("request_release_resources() failed!\n");
			result = ERROR_SYSTEM_CALL;
		}

		delete request;

		/*
		 * add the ID of the request that was reserved to the heap of reserved
		 * requests in order to use it again.
		 */
		if ( OPERATION_SUCCESS != dev->push_to_reserved_heap(id) )
		{
			ERR_PRINT("push_to_reserved_heap() failed!\n");
			result = ERROR_SYSTEM_CALL;
		}

		// if we encountered an error - stop operation
		if( OPERATION_SUCCESS != result )
		{
			break;
		}
	}


	if ( OPERATION_SUCCESS != result )
	{
		// we can only be here in case of error system
		cerr<< "system error" << endl;

		ERR_PRINT("daemon thread had an error!\n");

		dev->_daemon_thread_done_work = true;

		/*
		 * there was an error during the execution of the daemon thread,
		 * start releasing all the device resources without pay attention to
		 * system calls errors.
		 */
		dev->device_release_resources(true);

		exit( EXIT_ERROR );
	}


	if ( OPERATION_SUCCESS != dev->device_release_resources() )
	{
		cerr<< "system error" << endl;
		exit( EXIT_ERROR );
	}

	DBG_PRINT("daemon thread terminated!\n");

	dev->_daemon_thread_done_work = true;

	return NULL;
}
